package com.landun.market.service.impl;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.landun.market.exception.AuthenticationException;
import com.landun.market.exception.BusinessException;
import com.landun.market.exception.DataNotFoundException;
import com.landun.market.pojo.domain.AccessToken;
import com.landun.market.pojo.enums.ResponseCode;
import com.landun.market.pojo.enums.UserType;
import com.landun.market.pojo.model.User;
import com.landun.market.repository.IUserRepository;
import com.landun.market.service.ISecurityService;
import com.landun.market.service.IUserService;
import com.landun.market.util.NumberUtils;
import com.landun.market.util.PasswordUtils;
import com.landun.market.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author jiang.chuan
 * @date 2018-05-11
 */
@Slf4j
@Service
public class SecurityService implements ISecurityService {
    private final IUserRepository userRepository;
    private final IUserService userService;

    // 令牌有效期
    private final static int TOKEN_LIFECYCLE_MINUTES = 120;
    private final static int MIN_CAPACITY = 10, MAX_CAPACITY = 1_000;

    // the key is token, the value is userId
    private final static Cache<String, Integer> tokenToUserCache = Caffeine.newBuilder()
            .initialCapacity(MIN_CAPACITY).maximumSize(MAX_CAPACITY)
            .expireAfterAccess(TOKEN_LIFECYCLE_MINUTES, TimeUnit.MINUTES)
            .build();

    // the key is userId, the value is token
    private final static Cache<Integer, String> userToTokenCache = Caffeine.newBuilder()
            .initialCapacity(MIN_CAPACITY).maximumSize(MAX_CAPACITY)
            .expireAfterAccess(TOKEN_LIFECYCLE_MINUTES, TimeUnit.MINUTES)
            .build();

    @Autowired
    public SecurityService(IUserRepository userRepository, IUserService userService) {
        this.userRepository = userRepository;
        this.userService = userService;
    }

    @Override
    public AccessToken login(UserType userType, String identifier, String password, String host) {
        if (StringUtils.isBlank(identifier)) {
            throw new BusinessException(ResponseCode.PARAMETER_REQUIRED, "登陆的用户名");
        }

        // 用户类型
        int typeCode = Optional.ofNullable(userType).map(UserType::code).orElse(UserType.PLATFORM.code());

        // 根据用户名查询登录信息
        User user = userRepository.findByLogin(typeCode, identifier);

        // 没有找到用户, 或密码为空
        if (null == user || NumberUtils.isNotPositive(user.getId()) || StringUtils.isBlank(user.getPassword())) {
            log.warn("用户({})不存在", identifier);
            throw new AuthenticationException(ResponseCode.DATA_NOT_FOUND, "用户", identifier);
        }

        // 用户已经过期
        if (null != user.getExpiryTime() && user.getExpiryTime().before(new Date())) {
            log.warn("用户({})已经过期", identifier);
            throw new AuthenticationException(ResponseCode.DATA_NOT_FOUND, "用户", identifier);
        }

        // 验证密码
        if (!user.getPassword().equals(PasswordUtils.generateUserPassword(user.getId(), password))) {
            log.warn("用户({})密码不匹配", identifier);
            throw new AuthenticationException(ResponseCode.DATA_NOT_FOUND, "用户", identifier);
        }

        // 将原来的授权令牌失效
        invalidAccessToken(user.getId());

        // 生成授权令牌并缓存
        AccessToken accessToken = generateAccessToken(user);

        // 更新登陆信息
        userRepository.updateLastLoginInfo(user.getId(), host);

        return accessToken;

    }

    @Override
    public void logout(String token, String host) {
        // 将授权令牌标识成无效
        invalidAccessToken(token);
        log.info("注销授权令牌({})", token);
    }

    @Override
    public User getUserByToken(String token, String host) {
        Integer userId = tokenToUserCache.getIfPresent(StringUtils.trimToEmpty(token));
        if (NumberUtils.isNotPositive(userId)) {
            throw new DataNotFoundException(ResponseCode.DATA_NOT_FOUND, "用户", token);
        }

        return Optional.ofNullable(userService.load(userId))
                .orElseThrow(() -> new DataNotFoundException(ResponseCode.DATA_NOT_FOUND, "用户", userId));
    }

    @Override
    public Integer getUserIdByToken(String token, String host) {
        return tokenToUserCache.getIfPresent(StringUtils.trimToEmpty(token));
    }

    @Override
    public boolean isAuthenticated(String token, String host) {
        return NumberUtils.isPositive(tokenToUserCache.getIfPresent(StringUtils.trimToEmpty(token)));
    }

    // ======================== Private Method ========================

    /**
     * 生成授权令牌, 并缓存起来
     *
     * @param user 登录成功的用户信息
     * @return 授权令牌
     */
    private AccessToken generateAccessToken(User user) {
        // 生成授权令牌
        String token = DigestUtils.md5Hex(UUID.randomUUID().toString());
        AccessToken accessToken = AccessToken.builder()
                .accessToken(token)
                .expiresIn((int) TimeUnit.MINUTES.toSeconds(TOKEN_LIFECYCLE_MINUTES)).build();
        log.info("为用户({})生成授权令牌({})", user.getName(), token);

        // 生成 Token <-> UID 关系映射
        tokenToUserCache.put(token, user.getId());
        userToTokenCache.put(user.getId(), token);

        return accessToken;
    }

    /**
     * 将用户的令牌失效
     *
     * @param userId 用户ID
     */
    private void invalidAccessToken(int userId) {
        // 取出 Token, 并将 Token 设置成过期
        String originToken = userToTokenCache.getIfPresent(userId);
        if (StringUtils.isNotBlank(originToken)) {
            tokenToUserCache.invalidate(originToken);
        }

        // 将用户ID过期
        userToTokenCache.invalidate(userId);
    }

    /**
     * 将用户的令牌失效
     *
     * @param token 授权令牌
     */
    private void invalidAccessToken(String token) {
        // 取出 UserId, 并将 UserId 设置成过期
        Integer userId = tokenToUserCache.getIfPresent(token);
        if (NumberUtils.isPositive(userId)) {
            userToTokenCache.invalidate(userId);
        }

        // 将用户 Token 过期
        tokenToUserCache.invalidate(token);
    }
}
